/*
 * MySQL Cluster SNMP monitoring subagent
 * Copyright (C) 2011, Steven Ayre <steveayre@gmail.com>
 *
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is MySQL Cluster SNMP monitoring subagent
 *
 * The Initial Developer of the Original Code is
 * Steven Ayre <steveayre@gmail.com>
 * Portions created by the Initial Developer are Copyright (C)
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Steven Ayre <steveayre@gmail.com>
 *
 * subagent.c -- SNMP subagent thread code
 *
 */

#include <pthread.h>
#include <libdaemon/daemon.h>
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>
#include "ndb_snmpd.h"
#include "subagent.h"
#include "ndb_mgm.h"

pthread_mutex_t subagent_mutex = PTHREAD_MUTEX_INITIALIZER;

typedef struct {
	netsnmp_table_registration_info *tableinfo;
	netsnmp_tdata                   *table;
	netsnmp_handler_registration    *reginfo;
} node_table_t;

node_table_t clusterTable;
node_table_t mgmtNodeTable;
node_table_t dataNodeTable;
node_table_t apiNodeTable;

uint32_t idx;

static oid clusterTable_oid[]  = { 1,3,6,1,4,1,OID_IANA_ENTERPRISE,OID_STATUS,OID_CLUSTER_TABLE   };
static oid mgmtNodeTable_oid[] = { 1,3,6,1,4,1,OID_IANA_ENTERPRISE,OID_STATUS,OID_MGMT_NODE_TABLE };
static oid dataNodeTable_oid[] = { 1,3,6,1,4,1,OID_IANA_ENTERPRISE,OID_STATUS,OID_DATA_NODE_TABLE };
static oid apiNodeTable_oid[]  = { 1,3,6,1,4,1,OID_IANA_ENTERPRISE,OID_STATUS,OID_API_NODE_TABLE  };

static void nodeTable_free(netsnmp_cache *cache, node_table_t *table)
{
	netsnmp_tdata_row *row = netsnmp_tdata_row_first(table->table);

	/* Delete all rows, one by one */
	while(row) {
		netsnmp_tdata_remove_and_delete_row(table->table, row);
		free(row->data);
		row = netsnmp_tdata_row_first(table->table);
	}
}

static int clusterTable_load(netsnmp_cache *cache, void *vmagic)
{
	int i;
	clusterinfo_t *entry;
	netsnmp_tdata_row *row;

	/* Delete any previous table */
	nodeTable_free(cache, &clusterTable);

	/* Add monitored clusters to table */
	for(i=0; i<globals.num_clusters; i++) {
		cluster_t *cluster = &globals.clusters[i];

		/* Create row */
		entry = (clusterinfo_t*)calloc(1, sizeof(clusterinfo_t));
		entry->idx = cluster->info.idx;
		entry->name = strdup(cluster->info.name);
		entry->connectstring = strdup(cluster->info.connectstring);

		/* Add row */
		row = netsnmp_tdata_create_row();
		row->data = entry;
		netsnmp_tdata_row_add_index(row, ASN_INTEGER, &entry->idx, sizeof(entry->idx));
		netsnmp_tdata_add_row(clusterTable.table, row);
	}

	return 0;
}

static int nodeTable_load(netsnmp_cache *cache, node_table_t *table, int node_type)
{
	int i, j;
	ndb_node_entry_t *entry;
        netsnmp_tdata_row *row;

	/* Delete any previous table */
	nodeTable_free(cache, table);

	/* Add nodes for each monitored cluster to table */
	for(i=0; i<globals.num_clusters; i++) {
		cluster_t *cluster = &globals.clusters[i];
		ndb_node_table_t *nodes = NULL;

		/* Determine which node type is being put into the table */
		switch(node_type) {
			case 0: nodes = &cluster->ndb_nodes.mgmt; break;
			case 1: nodes = &cluster->ndb_nodes.data; break;
			case 2: nodes = &cluster->ndb_nodes.api;  break;
		}

		/* Add nodes to table, lock nodes to stop them being changed while we're adding them */
		pthread_mutex_lock(&cluster->ndb_nodes.mutex);
		for(j=0; j<nodes->num_nodes; j++) {
			/* Create row */
			entry = (ndb_node_entry_t*)malloc(sizeof(ndb_node_entry_t));
			memcpy(entry, &nodes->nodes[j], sizeof(ndb_node_entry_t));

			/* Add row */
		        row = netsnmp_tdata_create_row();
			row->data = entry;
			netsnmp_tdata_row_add_index(row, ASN_INTEGER, &entry->cluster_idx, sizeof(entry->cluster_idx));
			netsnmp_tdata_row_add_index(row, ASN_INTEGER, &entry->idx, sizeof(entry->idx));
		        netsnmp_tdata_add_row(table->table, row);
		}
		pthread_mutex_unlock(&cluster->ndb_nodes.mutex);
	}

	return 0;
}

static int mgmtNodeTable_load(netsnmp_cache *cache, void *vmagic) { return nodeTable_load(cache, &mgmtNodeTable, 0);  }
static int dataNodeTable_load(netsnmp_cache *cache, void *vmagic) { return nodeTable_load(cache, &dataNodeTable, 1);  }
static int apiNodeTable_load(netsnmp_cache *cache, void *vmagic)  { return nodeTable_load(cache, &apiNodeTable,  2);  }

static void clusterTable_free(netsnmp_cache *cache, void *vmagic)  { nodeTable_free(cache, &mgmtNodeTable); }
static void mgmtNodeTable_free(netsnmp_cache *cache, void *vmagic) { nodeTable_free(cache, &mgmtNodeTable); }
static void dataNodeTable_free(netsnmp_cache *cache, void *vmagic) { nodeTable_free(cache, &dataNodeTable); }
static void apiNodeTable_free(netsnmp_cache *cache, void *vmagic)  { nodeTable_free(cache, &apiNodeTable);  }

static int handle_clusterTable(netsnmp_mib_handler *handler, netsnmp_handler_registration *reginfo, netsnmp_agent_request_info *reqinfo, netsnmp_request_info *requests)
{
	netsnmp_request_info *request;
	netsnmp_table_request_info *table_info;
	clusterinfo_t *entry;
	
	switch (reqinfo->mode) {
	case MODE_GET:
		for (request = requests; request; request = request->next) {
			if (request->processed)
				continue;

			table_info = netsnmp_extract_table_info(request);
			entry = (clusterinfo_t *) netsnmp_tdata_extract_entry(request);

			switch (table_info->colnum) {
			case OID_CLUSTER_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->idx);
				break;
			case OID_CLUSTER_NAME:
				snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) entry->name, strlen(entry->name));
				break;
			case OID_CLUSTER_CONNECTSTRING:
				snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) entry->connectstring, strlen(entry->connectstring));
				break;
			case OID_CLUSTER_STATUS:
				snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) "n/a", strlen("n/a"));
				break;
			default:
				snmp_log(LOG_WARNING, "Unregistered clusterEntry OID-suffix requested (%d)\n", table_info->colnum);
				netsnmp_set_request_error(reqinfo, request, SNMP_NOSUCHOBJECT);
			}
		}
		break;
	default:
		/* we should never get here, so this is a really bad error */
		snmp_log(LOG_ERR, "Unknown mode (%d) in handle_channelList\n", reqinfo->mode );
		return SNMP_ERR_GENERR;
	}

	return SNMP_ERR_NOERROR;
}

static int handle_mgmtNodeTable(netsnmp_mib_handler *handler, netsnmp_handler_registration *reginfo, netsnmp_agent_request_info *reqinfo, netsnmp_request_info *requests)
{
	netsnmp_request_info *request;
	netsnmp_table_request_info *table_info;
	ndb_node_entry_t *entry;
	
	switch (reqinfo->mode) {
	case MODE_GET:
		for (request = requests; request; request = request->next) {
			if (request->processed)
				continue;

			table_info = netsnmp_extract_table_info(request);
			entry = (ndb_node_entry_t *) netsnmp_tdata_extract_entry(request);

			switch (table_info->colnum) {
			case OID_MGMT_NODE_CLUSTER_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->cluster_idx);
				break;
			case OID_MGMT_NODE_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->idx);
				break;
			case OID_MGMT_NODE_ID:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->id);
				break;
			case OID_MGMT_NODE_INETADDRESSTYPE:
				if (entry->addr_family == AF_INET6) {
					snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, INETADDRESSTYPE_IPV6);
				} else {
					snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, INETADDRESSTYPE_IPV4);
				}
				break;
			case OID_MGMT_NODE_INETADDRESS:
				if (entry->addr_family == AF_INET6) {
					snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) &entry->ip_addr.v6, sizeof(entry->ip_addr.v6));
				} else {
					snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) &entry->ip_addr.v4, sizeof(entry->ip_addr.v4));
				}
				break;
			case OID_MGMT_NODE_VERSION:
				snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) entry->version, strlen(entry->version));
				break;
			default:
				snmp_log(LOG_WARNING, "Unregistered mgmtNodeEntry OID-suffix requested (%d)\n", table_info->colnum);
				netsnmp_set_request_error(reqinfo, request, SNMP_NOSUCHOBJECT);
			}
		}
		break;
	default:
		/* we should never get here, so this is a really bad error */
		snmp_log(LOG_ERR, "Unknown mode (%d) in handle_channelList\n", reqinfo->mode );
		return SNMP_ERR_GENERR;
	}

	return SNMP_ERR_NOERROR;
}

static int handle_dataNodeTable(netsnmp_mib_handler *handler, netsnmp_handler_registration *reginfo, netsnmp_agent_request_info *reqinfo, netsnmp_request_info *requests)
{
	netsnmp_request_info *request;
	netsnmp_table_request_info *table_info;
	ndb_node_entry_t *entry;
	
	switch (reqinfo->mode) {
	case MODE_GET:
		for (request = requests; request; request = request->next) {
			if (request->processed)
				continue;

			table_info = netsnmp_extract_table_info(request);
			entry = (ndb_node_entry_t *) netsnmp_tdata_extract_entry(request);

			switch (table_info->colnum) {
			case OID_DATA_NODE_CLUSTER_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->cluster_idx);
				break;
			case OID_DATA_NODE_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->idx);
				break;
			case OID_DATA_NODE_ID:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->id);
				break;
			case OID_DATA_NODE_INETADDRESSTYPE:
				if (entry->addr_family == AF_INET6) {
					snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, INETADDRESSTYPE_IPV6);
				} else {
					snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, INETADDRESSTYPE_IPV4);
				}
				break;
			case OID_DATA_NODE_INETADDRESS:
				if (entry->addr_family == AF_INET6) {
					snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) &entry->ip_addr.v6, sizeof(entry->ip_addr.v6));
				} else {
					snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) &entry->ip_addr.v4, sizeof(entry->ip_addr.v4));
				}
				break;
			case OID_DATA_NODE_VERSION:
				snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) entry->version, strlen(entry->version));
				break;
			case OID_DATA_NODE_NODEGROUP:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->nodegroup);
				break;
			default:
				snmp_log(LOG_WARNING, "Unregistered dataNodeEntry OID-suffix requested (%d)\n", table_info->colnum);
				netsnmp_set_request_error(reqinfo, request, SNMP_NOSUCHOBJECT);
			}
		}
		break;
	default:
		/* we should never get here, so this is a really bad error */
		snmp_log(LOG_ERR, "Unknown mode (%d) in handle_channelList\n", reqinfo->mode );
		return SNMP_ERR_GENERR;
	}

	return SNMP_ERR_NOERROR;
}

static int handle_apiNodeTable(netsnmp_mib_handler *handler, netsnmp_handler_registration *reginfo, netsnmp_agent_request_info *reqinfo, netsnmp_request_info *requests)
{
	netsnmp_request_info *request;
	netsnmp_table_request_info *table_info;
	ndb_node_entry_t *entry;
	
	switch (reqinfo->mode) {
	case MODE_GET:
		for (request = requests; request; request = request->next) {
			if (request->processed)
				continue;

			table_info = netsnmp_extract_table_info(request);
			entry = (ndb_node_entry_t *) netsnmp_tdata_extract_entry(request);

			switch (table_info->colnum) {
			case OID_API_NODE_CLUSTER_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->cluster_idx);
				break;
			case OID_API_NODE_INDEX:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->idx);
				break;
			case OID_API_NODE_ID:
				snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, entry->id);
				break;
			case OID_API_NODE_INETADDRESSTYPE:
				if (entry->addr_family == AF_INET6) {
					snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, INETADDRESSTYPE_IPV6);
				} else {
					snmp_set_var_typed_integer(request->requestvb, ASN_INTEGER, INETADDRESSTYPE_IPV4);
				}
				break;
			case OID_API_NODE_INETADDRESS:
				if (entry->addr_family == AF_INET6) {
					snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) &entry->ip_addr.v6, sizeof(entry->ip_addr.v6));
				} else {
					snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) &entry->ip_addr.v4, sizeof(entry->ip_addr.v4));
				}
				break;
			case OID_API_NODE_VERSION:
				snmp_set_var_typed_value(request->requestvb, ASN_OCTET_STR, (u_char *) entry->version, strlen(entry->version));
				break;
			default:
				snmp_log(LOG_WARNING, "Unregistered apiNodeEntry OID-suffix requested (%d)\n", table_info->colnum);
				netsnmp_set_request_error(reqinfo, request, SNMP_NOSUCHOBJECT);
			}
		}
		break;
	default:
		/* we should never get here, so this is a really bad error */
		snmp_log(LOG_ERR, "Unknown mode (%d) in handle_channelList\n", reqinfo->mode );
		return SNMP_ERR_GENERR;
	}

	return SNMP_ERR_NOERROR;
}

static int snmp_subagent_create_table(const char *table_name, oid *table_oid, int table_oid_len, node_table_t *table, Netsnmp_Node_Handler *handler, int min, int max, NetsnmpCacheLoad *load_handler, NetsnmpCacheFree *free_handler)
{
	memset(table, 0, sizeof(table));

	if (!(table->tableinfo = (netsnmp_table_registration_info*)calloc(1, sizeof(netsnmp_table_registration_info)))) {
		goto err;
	}
	netsnmp_table_helper_add_indexes(table->tableinfo, ASN_INTEGER, 0);
	table->tableinfo->min_column = min;
	table->tableinfo->max_column = max;

	if (!(table->table = netsnmp_tdata_create_table(table_name, 0))) {
		goto err;
	}

	if (!(table->reginfo = netsnmp_create_handler_registration(table_name, handler, table_oid, table_oid_len, HANDLER_CAN_RONLY))) {
		goto err;
	}

	if (netsnmp_tdata_register(table->reginfo, table->table, table->tableinfo) < 0) {
		goto err;
	}

	if (netsnmp_inject_handler(table->reginfo, netsnmp_get_cache_handler(5, load_handler, free_handler, table_oid, table_oid_len)) < 0) {
		goto err;
	}

	return 0;

  err:
	/* TODO cleanup */
	return -1;
}

static int snmp_callback_log(int major, int minor, void *serverarg, void *clientarg)
{
	struct snmp_log_message *slm = (struct snmp_log_message *) serverarg;
	thread_t *thread = (thread_t*)clientarg;
	int len;

	len = strlen(slm->msg);
	if (len>0 && slm->msg[len-1] == '\n') len--;
	if (len>0 && slm->msg[len-1] == '\r') len--;
	daemon_log(slm->priority, "%s: %.*s", thread_name(thread), len, slm->msg);

	return SNMP_ERR_NOERROR;
}
int snmp_subagent_main(thread_t *thread, void *param)
{
	int status = 0;

	/* Net-SNMP is not thread-safe, so make sure that only one subagent thread is running at a time */
	if (pthread_mutex_trylock(&subagent_mutex) != 0) {
		return -1;
	}

	/* Initialise the subagent */
	snmp_register_callback(SNMP_CALLBACK_LIBRARY, SNMP_CALLBACK_LOGGING, snmp_callback_log, thread);
	snmp_enable_calllog();
	netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, NETSNMP_DS_LIB_DONT_PERSIST_STATE, 1);
	netsnmp_ds_set_boolean(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_ROLE, 1);
        init_agent("ndb_snmpd");
        netsnmp_ds_set_int(NETSNMP_DS_APPLICATION_ID, NETSNMP_DS_AGENT_AGENTX_PING_INTERVAL, 2);
        init_snmp("ndb_snmpd");

	/* Register OID handlers */
	if (snmp_subagent_create_table("clusterTable", clusterTable_oid, OID_LENGTH(clusterTable_oid), &clusterTable, handle_clusterTable, OID_CLUSTER_MIN, OID_CLUSTER_MAX, clusterTable_load, clusterTable_free) < 0) {
		status = -1;
		goto end;
	}
	if (snmp_subagent_create_table("mgmtNodeTable", mgmtNodeTable_oid, OID_LENGTH(mgmtNodeTable_oid), &mgmtNodeTable, handle_mgmtNodeTable, OID_MGMT_NODE_MIN, OID_MGMT_NODE_MAX, mgmtNodeTable_load, mgmtNodeTable_free) < 0) {
		status = -1;
		goto end;
	}
	if (snmp_subagent_create_table("dataNodeTable", dataNodeTable_oid, OID_LENGTH(dataNodeTable_oid), &dataNodeTable, handle_dataNodeTable, OID_DATA_NODE_MIN, OID_DATA_NODE_MAX, dataNodeTable_load, dataNodeTable_free) < 0) {
		status = -1;
		goto end;
	}
	if (snmp_subagent_create_table("apiNodeTable", apiNodeTable_oid, OID_LENGTH(apiNodeTable_oid), &apiNodeTable, handle_apiNodeTable, OID_API_NODE_MIN, OID_API_NODE_MAX, apiNodeTable_load, apiNodeTable_free) < 0) {
		status = -1;
		goto end;
	}

	/* Handle SNMP requests */
	while(thread_running(thread)) {
		if (agent_check_and_process(1) < 0) {
			status = -1;
			goto end;
		}
		thread_yield(thread, 5000);
	}

  end:
	/* Cleanup */
	/* TODO cleanup tables */
	snmp_shutdown("ndb_snmpd");

	// Return the status and unlock the snmp functionality
	pthread_mutex_unlock(&subagent_mutex);
	return status;
}

